<?php

namespace slinks\common\dynamic;

class DynamicPhpManager {

    private static $instance;
    private $cacheDirectory = '';
    private $debug = false;

    private function __construct() {
        
    }

    /**
     * Returns the singleton instance of this object
     * @return Slinks
     */
    public static function getInstance() {
        // if not yet intialized
        if (!isset(self::$instance)) {
            self::$instance = new DynamicPhpManager();
        }
        return self::$instance;
    }

    /**
     * Initializes the dynamic php from a cached file or requests the dynamic file then caches it for future use. 
     * If debug is true and the cache file exists it is checked to make sure it is current. 
     * If debug is false then the cached file is alwaysed used if it exists
     * @param IDynamic $dynamic
     */
    public function initialize(IDynamic $dynamic) {

        $location = $this->getLocation($dynamic);

        $reload = $this->isDebug() ? $this->needsReload($location) : false;

        if ($reload || !file_exists($location . '.php')) {
            $this->build($dynamic);
        }

        require_once $location . '.php';
    }

    /**
     * Builds the dynamic php and caches them to a file. 
     * @param IDynamic $dynamic the object to build
     * @throws Exception
     */
    private function build(IDynamic $dynamic) {

        $dir = $this->getCacheDirectory();
        if (!is_dir($dir)) {
            if (false === @mkdir($dir, 0777, true)) {
                throw new \Exception(sprintf('Unable to create the Cache directory (%s)', dirname($dir)));
            }
        } elseif (!is_writable($dir)) {
            throw new \Exception(sprintf('Unable to write in the Cache directory (%s)', $dir));
        }

        $content = $dynamic->getPhp();
        // cache the class
        if (!$this->isDebug()) {
            $content = $this->stripComments($content);
        }

        $location = $this->getLocation($dynamic);
        $file = $location . '.php';
        $this->writeCacheFile($file, $content);

        if ($this->isDebug()) {
            // save the resources
            $this->writeCacheFile($location . '.meta', serialize($dynamic->getResources()));
        }
    }

    private function needsReload($location) {

        if (!file_exists($location . '.meta') || !file_exists($location . '.php')) {
            return true;
        }

        $meta = unserialize(file_get_contents($location . '.meta'));
        $time = filemtime($location . '.php');
        foreach ($meta as $resource) {
            if (!$resource->isFresh($time)) {
                return true;
            }
        }
        return false;
    }

    private function getLocation(IDynamic $dynamic) {
        return $this->getCacheDirectory() . DIRECTORY_SEPARATOR . $dynamic->getClassName() . ($this->isDebug() ? '_Debug' : '');
    }

    /**
     * Removes comments from a PHP source string.
     *
     * We don't use the PHP php_strip_whitespace() function
     * as we want the content to be readable and well-formatted.
     *
     * @param string $source A PHP string
     *
     * @return string The PHP string with the comments removed
     */
    private function stripComments($source) {
        if (!function_exists('token_get_all')) {
            return $source;
        }

        $output = '';
        foreach (token_get_all($source) as $token) {
            if (is_string($token)) {
                $output .= $token;
            } elseif (!in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
                $output .= $token[1];
            }
        }
        // replace multiple new lines with a single newline
        $output = preg_replace(array('/\s+$/Sm', '/\n+/S'), "\n", $output);

        // reformat {} "a la python"
        $output = preg_replace(array('/\n\s*\{/', '/\n\s*\}/'), array(' {', ' }'), $output);

        return $output;
    }

    private function writeCacheFile($file, $content) {
        $tmpFile = tempnam(dirname($file), basename($file));
        if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) {
            chmod($file, 0644);
            return;
        }
        throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file));
    }

    public function getCacheDirectory() {
        return $this->cacheDirectory;
    }

    public function setCacheDirectory($cacheDirectory) {
        $this->cacheDirectory = $cacheDirectory;
    }

    public function isDebug() {
        return $this->debug;
    }

    public function setDebug($debug) {
        $this->debug = $debug;
    }

}